package com.seckill.order.lock;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ThreadUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisLockImpl implements RedisLock {
    private CountDownLatch countDownLatch;

    public void setCountDownLatch(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    //private RedisTemplate<String, String> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public RedisLockImpl(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 加锁
     *
     * @param key
     * @param timeout
     * @param unit
     * @return
     */
    @Override
    public boolean tryLock(String key, Long timeout, TimeUnit unit) {
        //1. 获取线程唯一标识 UUID:现成ID
        String threadId = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
        threadLocal.set(threadId);//将线程唯一表示存入本地线程中
        //2. 将现成唯一标识和锁进行绑定
        Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, timeout, unit);

        return ifAbsent;
    }

    /**
     * 自动续期加锁
     *
     * @param key
     * @return
     */
    @Override
    public boolean tryLock(String key) {

        //加锁
        boolean isLock = this.tryLock(key, 30L, TimeUnit.SECONDS);
        log.info("尝试加锁,加锁结果为: {}", isLock);

        //如果加锁成功,才需要分布式锁进行续期
        if (isLock) {
            log.info("加锁成功,开始子线程进行锁续期");
            //开辟子线程续期,可称为看门口线程,续命线程等等
            new Thread(() -> {
//                String redis_threadId = stringRedisTemplate.opsForValue().get(key);
//                log.info("开始执行锁续期, 线程id为: {}", Thread.currentThread().getId());
//                long count = 0L;
//                while (!Thread.currentThread().isInterrupted()){//没有中断,这个续期就正常周期续期
//                    try {
//                        Thread.sleep(5000);//每20秒续期//一般是过期时间的三分之一
//                        stringRedisTemplate.expire(key, 30L, TimeUnit.SECONDS);
//                        if (ObjectUtil.isEmpty(redis_threadId)) {//如果分布式锁都没有,就没有必要续期
//                            return;
//                        }
//                        //不能放到while外面,因为如果续期后,此键值对的有效期因为固定,可能会导致过期,到释放锁的时候要用的时候就没了,所以只能每次都再重新设置一下这个键值对
//                        //以处理下单的线程唯一标识为键,续期线程id为值,缓存续期线程id,以便后续释放锁的时候可以将续期线程释放掉(interrupt)
//                        stringRedisTemplate.opsForValue().set(redis_threadId, Thread.currentThread().getId() + "", 30L, TimeUnit.SECONDS);
//                        log.info("成功续期, 当前续期次数为: {}, 线程id为: {}",++count, Thread.currentThread().getId());
//                    } catch (InterruptedException e) {
//                        Thread.currentThread().interrupt();
//                    }
//                }
                new LockRenewAsync(stringRedisTemplate, key, 30L, TimeUnit.SECONDS, countDownLatch);
            }).start();

        }
        return isLock;
    }

    /**
     * 重试机制-锁阻塞:获取锁失败后,可以继续尝试一定次数或者事件后才会被挂起或者放弃获取
     *
     * @param waitTime 等待时间
     * @param unit     时间单位
     * @param key      分布式锁的键
     * @return
     */
    @Override
    public boolean tryLock(Long waitTime, TimeUnit unit, String key) {
        //尝试加锁
        boolean isLock = tryLock(key, 30L, TimeUnit.SECONDS);

        //如果加锁失败了,阻塞重试一段时间
        if (!isLock) {
            try {
                log.info("加锁失败,进行阻塞重试--------");
                Long waitedTime = 0L;//已经等待的时间
                Long count = 0L;
                while (unit.toMillis(waitTime) > waitedTime) {//就类似自旋
                    Thread.sleep(100);
                    log.info("开始进行重试,重试次数为: {}", ++count);
                    //阻塞100毫秒,相当于每100ms重新尝试获取锁,实际redisson是根据执行的时长来进行获取的
                    isLock = tryLock(key, 30L, TimeUnit.SECONDS);//这个没有续期重载方法只有一个参数key的是有续期功能的
                    waitedTime += 100;
                    if (isLock) {
                        log.info("重试获取锁成功,重试次数为: {}", ++count);
                        break;//成功获取锁就退出阻塞重试
                    }

                    log.info("重试获取锁失败, 重试次数为: {}", ++count);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }

        //如果加锁成功,需要给分布式锁进行续期
        if (isLock) {
            log.info("加锁成功开启线程续期子线程(模仿看门狗), 线程为: {}", Thread.currentThread().getId());
            new Thread(new LockRenewAsync(stringRedisTemplate, key, 30L, TimeUnit.SECONDS, countDownLatch)).start();
        }

        return isLock;
    }

    /**
     * 释放锁
     *
     * @param key
     */
    @Override
    public void unLock(String key) throws InterruptedException {
        String redis_threadId = stringRedisTemplate.opsForValue().get(key);
        String threadId = threadLocal.get();

        //判断是否一样,
        if (ObjectUtil.isEmpty(threadId) || ObjectUtil.notEqual(threadId, redis_threadId)) {
            log.info("不能释放非本线程的锁!");
            return;
        }

        stringRedisTemplate.delete(key);//删除这个商品的分布式锁,避免OOM
        threadLocal.remove();//删除本地线
        countDownLatch.await();
        // 程中存储的本线程的唯一识别码
        log.info("分布式锁释放成功, 现成唯一识别码为:{}", threadId);
        log.info("当前执行操作的线程为: {}", Thread.currentThread().getId());


        String flushThreadId = stringRedisTemplate.opsForValue().get(redis_threadId);

        if (StrUtil.isNotEmpty(flushThreadId)) {
            log.info("开始停止续期, 续期线程id为: {}", flushThreadId);
            Thread flushThread = ThreadUtils.findThreadById(Convert.toLong(flushThreadId));
            flushThread.interrupt();//终止线程
            stringRedisTemplate.delete(redis_threadId);//删除这个刷新线程,避免OOM
            log.info("重试线程被释放,停止续期, 续期线程id为: {}", flushThreadId);
        }
    }
}
